﻿/*
Copyright (c) 2010 Lance Ward
This software is licensed under the MIT license, except where otherwise noted.
For complete license details, refer to license.txt
*/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace ADNTimeline
{
	public partial class FrameItem : Control
	{
		const int frameWidth = 12;
		const int sizerWidth = 3;
		private int frameOffset = 0;
		private int selFrameIndex = -1;	//nothing selected initially
		private List<TimeLineFrame> frames = new List<TimeLineFrame>();
		private bool isCapturing = false;
		private bool isSizing = false;
		private Bitmap framesImage;
        static Color selFrameColor = Color.LightBlue;
        static Color frameColor = Color.DarkGray;
        private bool selected = false;
        private int startDragX = -1;

		public FrameItem()
		{
			InitializeComponent();
            if (this.Height > 0 && this.Width > 0) { framesImage = new Bitmap(this.Width, this.Height); }
		}

        public bool Selected
        {
            get { return selected; }
            set
            {
                selected = value;
                this.Invalidate();
            }
        }

		public int FrameOffset
		{
			get { return frameOffset; }
			set
			{
				frameOffset = value;
				this.Invalidate();
			}
		}

        public int SelectedFrame
        {
            get { return selFrameIndex; }
        }

        public List<TimeLineFrame> Frames
        {
            get { return frames; }
        }

		public event EventHandler FrameAdded;
		public event EventHandler FrameDeleted;
		public event EventHandler FrameMoved;
		public event EventHandler ItemSelected;

		private void OnItemSelected()
		{
			if (ItemSelected != null) { ItemSelected(this, EventArgs.Empty); }
		}

		private void OnFrameAdded()
		{
			if (FrameAdded != null) { FrameAdded(this, EventArgs.Empty); }
		}

		private void OnFrameDeleted()
		{
			if (FrameDeleted != null) { FrameDeleted(this, EventArgs.Empty); }
		}

		private void OnFrameMoved()     //called on both moves and resizes (change of span)
		{
			if (FrameMoved != null) { FrameMoved(this, EventArgs.Empty); }
		}

		private void TimeLineItem_MouseDoubleClick(object sender, MouseEventArgs e)
		{
			//add a frame if it doesn't exist in the current location
			int hitFrame = HitTestFrames(e.X, e.Y);
			if (hitFrame == -1)
			{
				InsertFrame(e.X, e.Y);
			}
			else
			{
				selFrameIndex = hitFrame;
			}
			this.Invalidate();
            OnItemSelected();
		}

		private void TimeLineItem_MouseDown(object sender, MouseEventArgs e)
		{
			int hitFrame = HitTestFrames(e.X, e.Y);
			if (hitFrame != -1)
			{
				selFrameIndex = hitFrame;
				isCapturing = true;

				if (this.Cursor == Cursors.SizeWE)
				{
					isSizing = true;
				}

                startDragX = e.X;
			}
			this.Invalidate();
            OnItemSelected();
		}

		private void TimeLineItem_MouseMove(object sender, MouseEventArgs e)
		{
			if (isCapturing)
			{
				Rectangle rect = new Rectangle();
				if (isSizing)
				{
					int frameX = GetFrameX(selFrameIndex);
					rect.X = frameX;
					rect.Y = 0;
					rect.Width = e.X - frameX;
					rect.Height = this.Height;
				}
				else	//moving
				{
                    int xDiff = startDragX != -1 ? e.X - startDragX : 0;
                    rect.X = GetFrameX(selFrameIndex) + xDiff;
					rect.Y = 0;
					rect.Width = frames[selFrameIndex].Span * frameWidth;
					rect.Height = this.Height;
				}

				Graphics g = Graphics.FromHwnd(this.Handle);
				g.DrawImage(framesImage, 0, 0);
				Brush brush = new SolidBrush(Color.Red);
				g.FillRectangle(brush, rect);
			}
			else
			{
				//check for edges and change the cursor
				this.Cursor = GetSizeCursor(e.X, e.Y);
			}
		}

		private void TimeLineItem_MouseUp(object sender, MouseEventArgs e)
		{
			if (isCapturing)
			{
				if (isSizing)  //changing frame span
				{
                    int clickedFrame = GetClickedFrame(e.X, e.Y);
                    int overlap = GetOverlap(clickedFrame, clickedFrame + frames[selFrameIndex].Span - 1); //span includes the first cell, subtract it
                    if (overlap == 0) { frames[selFrameIndex].Span = Math.Max(clickedFrame - frames[selFrameIndex].StartCell, 1); }
                    else { }
                    
				}
				else //moving, dragging	TODO: change index of frame - use GetFrameIndex
				{
                    int xDiff = startDragX != -1 ? e.X - startDragX : 0;
                    int clickedFrame = GetClickedFrame(GetFrameX(selFrameIndex) + xDiff, e.Y);
                    int overlap = GetOverlap(clickedFrame, clickedFrame + frames[selFrameIndex].Span - 1); //span includes the first cell, subtract it
                    if (overlap == 0) { frames[selFrameIndex].StartCell = clickedFrame; }
                    else { }
				}

				OnFrameMoved();
			}

			isCapturing = false;
			isSizing = false;
            startDragX = -1;

			Redraw();
			this.Invalidate();
		}
        
		//for use in sizing frames
		private int GetMaxSpan(int frameIndex)
		{
			//last frame, can be as long as it wants to be
			if (frameIndex == frames.Count - 1) { return -1; }
			else
			{
				return frames[frameIndex + 1].StartCell - frames[frameIndex].StartCell;
			}
		}

		//for use in moving frames - determines if the new index location has enough room to hold the frame
		private int GetFreeSpan(int frameIndex, int destIndex)
		{
			//return the available space between destIndex and destIndex + 1;

			return 0;
		}

		//returns 0 if no overlap or a number of frames.
		//OLD - DEPRECATED - DELETE AFTER MIGRATION
        private int GetOverlap(int startFrame, int endFrame)
        {
            for (int i = 0; i < frames.Count; i++)
            {
                if (i != selFrameIndex)
                {
                    if (InRange(startFrame, frames[i].StartCell, frames[i].StartCell + frames[i].Span - 1))
                    {
                        return Math.Abs(frames[i].StartCell - startFrame);
                    }

                    if (InRange(endFrame, frames[i].StartCell, frames[i].StartCell + frames[i].Span - 1))
                    {
                        return Math.Abs(frames[i].StartCell - endFrame - 1);
                    }
                }
            }
            return 0;
        }

        private bool InRange(int value, int start, int end)
        {
            if (value >= start && value <= end) { return true; }
            return false;
        }

		private void ShiftFrames()
		{

		}

		private void TimeLineItem_Paint(object sender, PaintEventArgs e)
		{
			Redraw();
			e.Graphics.DrawImage(framesImage, 0, 0);
		}

		private void TimeLineItem_Resize(object sender, EventArgs e)
		{
            if (framesImage != null)
            {
                framesImage.Dispose();
                framesImage = null;
            }

            if (this.Height > 0 && this.Width > 0)
            {
                framesImage = new Bitmap(this.Width, this.Height);
                Redraw();
                this.Invalidate();
            }
		}

		private void Redraw()
		{
			Graphics g = Graphics.FromImage(framesImage);
			g.Clear(Color.White);
			DrawFrameMarkers(g);
			DrawFrames(g);
			DrawBottomOutline(g);
		}

		private void DrawFrames(Graphics g)
		{

			Brush brush = new SolidBrush(frameColor);
            Brush selBrush = selected ? new SolidBrush(selFrameColor) : new SolidBrush(frameColor);
			Rectangle rect = new Rectangle();

			rect.Y = 0;
			rect.Height = this.Height;

			for (int i = 0; i < frames.Count; i++)
			{
				rect.X = frameWidth * (frames[i].StartCell - frameOffset);
				rect.Width = frameWidth * frames[i].Span;

				if (i == selFrameIndex)
				{
					g.FillRectangle(selBrush, rect);

				}
				else
				{
					g.FillRectangle(brush, rect);
				}

				DrawSizer(g, rect);
			}
		}

		private void DrawSizer(Graphics g, Rectangle rect)
		{
			Rectangle sizer = new Rectangle();
			sizer.X = rect.Right - sizerWidth - 2;
			sizer.Y = (rect.Bottom / 4);
			sizer.Height = ((rect.Bottom / 4) * 3) - 2;
			sizer.Width = sizerWidth;

			Brush b = new SolidBrush(Color.White);
			g.FillRectangle(b, sizer);
			g.DrawRectangle(new Pen(Color.Black), sizer);
		}

		private void DrawFrameMarkers(Graphics g)
		{
			Brush evenBrush = new SolidBrush(Color.FromArgb(245, 245, 245));
			Brush oddBrush = new SolidBrush(Color.White);
			Rectangle rect = new Rectangle();

			rect.X = 0;
			rect.Y = 0;
			rect.Height = this.Height;
			rect.Width = frameWidth;

			bool toggle = false;

			for (int i = 0; i < this.Width; i += frameWidth)
			{
				rect.X = i;
				if (toggle)	//even
				{
					g.FillRectangle(evenBrush, rect);
					toggle = false;
				}
				else
				{
					g.FillRectangle(oddBrush, rect);
					toggle = true;
				}
			}
		}

		private void DrawBottomOutline(Graphics g)
		{
			Pen p = new Pen(Color.DarkGray, 1.0f);
			g.DrawLine(p, new Point(0, framesImage.Height - 1),
				new Point(framesImage.Width, framesImage.Height - 1));
		}

		public int InsertFrame(int x, int y)
		{
			TimeLineFrame tlf = new TimeLineFrame();
			//convert coordinates to the current frame
			int clickedFrame = GetClickedFrame(x, y);
			//get the index for the frame

			tlf.StartCell = clickedFrame;
			tlf.Span = 1;

			int index = GetFrameIndex(clickedFrame);
			//insert at the determined index.
			if (index >= 0)
			{
				frames.Insert(index, tlf);
			}
			else
			{
				frames.Add(tlf);
				index = 0;
			}

			//return the index
			selFrameIndex = index;
			//set the selected frame as inserted Frame
			this.Invalidate();
			OnFrameAdded();
			return index;
		}

		private Cursor GetSizeCursor(int x, int y)
		{
			int hitFrame = HitTestFrames(x, y);
			if (hitFrame != -1)
			{
				if (HitTestSizer(x, y, hitFrame))
				{
					return Cursors.SizeWE;
				}
				else
				{
					return Cursors.Hand;
				}
			}

			return Cursors.Default;

		}

		private int GetClickedFrame(int x, int y)
		{
            float closestFrame = ((float)x / (float)frameWidth) + (float)frameOffset;
			return (int)Math.Floor(closestFrame);
		}

		private int HitTestFrames(int x, int y)
		{
			int clickedFrame = GetClickedFrame(x, y);

			for (int i = 0; i < frames.Count; i++)
			{
				if (clickedFrame >= frames[i].StartCell && clickedFrame < frames[i].StartCell + frames[i].Span)
				{
					return i;
				}
			}

			return -1;
		}

		private int GetFrameX(int frameIndex)
		{
			if (frameIndex >= 0 && frameIndex < frames.Count)
			{
				int frameX = (frames[frameIndex].StartCell - frameOffset) * frameWidth;		//TODO: subtract frameOffset from StartCell first
				return frameX;
			}

			return -1;
		}

		private bool HitTestSizer(int x, int y, int frameIndex)
		{
			int frameX = GetFrameX(frameIndex);
			int currFrameWidth = frameWidth * frames[frameIndex].Span;
			Rectangle sizer = new Rectangle();
			sizer.X = frameX + currFrameWidth - sizerWidth - 2;
			sizer.Y = (this.Height / 4);
			sizer.Height = ((this.Height / 4) * 3) - 2;
			sizer.Width = sizerWidth;

			if (sizer.Contains(x, y))
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		private int GetFrameIndex(int clickedFrame)
		{
			for (int i = 0; i < frames.Count; i++)
			{
				if (clickedFrame == frames[i].StartCell || (clickedFrame > frames[i].StartCell && clickedFrame < frames[i].StartCell + frames[i].Span - 1))
				{
					return i;
				}
				else if (clickedFrame < frames[i].StartCell)
				{
					return i;
				}
			}

			//if we get here, the click happened after all current frames
			return frames.Count;    //do not subtract one because the frame has not been added yet.
		}
	}

	public class TimeLineFrame
	{
		private int startCell = 0;
		private int span = 1;
		public int StartCell
		{
			get { return startCell; }
			set { startCell = value; }
		}

		public int Span
		{
			get { return span; }
			set { span = value; }
		}
	}

	public class FrameItemEventArgs : EventArgs
	{
		public FrameItemEventArgs() { }
		public int FrameIndex { get; set; }
		public int FrameStart { get; set; }
		public int FrameEnd { get; set; }
	}
}
